package com.myebills.ebpsfrontend.cust.controller;

import java.io.Serializable;
import java.util.Hashtable;
import java.net.URLEncoder;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpUtils;

/**
 * This Java Bean manages optional encryption and decryption of inter-page URL's.
 *
 * This bean must be constructed by a controlling servlet so that it can retain
 * references to the relevant <code>HttpServletRequest</code> and
 * <code>HttpServletResponse</code> objects.
 * The controlling servlet should then attach this bean to the <em>request</em>
 * scope for access by an associated Java Server Page.
 */

public class EbppxUrlManager implements Serializable {

    /**
     * The attribute name used to store an instance of this class
     * in the request scope for access by Java Server Pages.
     */
    public static final String REQUEST_ATTRIBUTE_NAME = "EbppxUrlManager";

    /**
     * The parameter setting
     * (for the value of the parameter named with <b>PARAM_CIPHER_ALGORITHM</b>)
     * that indicates that URL's should appear in plain-text.
     */
    public static final String NULL_CIPHER_ALGORITHM = "NONE";

    /**
     * The parameter name for the desired URL encryption algorithm.
     * The recommended value for the parameter is "3DES/PCBC/PKCS5".
     */
    public static final String PARAM_CIPHER_ALGORITHM = "cipherAlgorithm";

    /**
     * The parameter name for the encryption key.
     * For the recommended cipher algorithm of "3DES/PCBC/PKCS5",
     * a 24-character key is required.
     * (For the "null" cipher algorithm, this parameter is ignored.)
     */
    public static final String PARAM_CIPHER_KEY = "cipherKey";

    /**
     * The parameter name that indicates if <em>session integrity</em>
     * protection is used on URL's to prevent cross-session
     * browser bookmarking.
     */
    public static final String PARAM_SESSION_INTEGRITY = "useSessionIntegrity";

    /**
     * Default name of the request parameter to be used to hold
     * the encrypted value of the entire actual query parameter string.
     */
    public static final String DEFAULT_QUERY_ENCRYPTION_PARAMETER_NAME = "q";

    /**
     * Default name of the encrypted request parameter to be used to hold
     * the session integrity value.
     * This value is included in each encrypted query string to prevent
     * use of browser-bookmarked URL's from a prior session.
     */
    public static final String DEFAULT_SESSION_INTEGRITY_PARAMETER_NAME = "si";

    private String queryEncryptionParameterName =
            DEFAULT_QUERY_ENCRYPTION_PARAMETER_NAME;

    private String sessionIntegrityParameterName =
            DEFAULT_SESSION_INTEGRITY_PARAMETER_NAME;

    private HttpServletRequest request;
    private HttpServletResponse response;

    private String cipherAlgorithm;
    private String cipherKey;
    private boolean useSessionIntegrity;

    private String sessionIntegrityValue = "";

    private Hashtable decryptedParameters;

    /**
     * No-argument constructor intended only for use by Java Beans
     * serialization mechanism.
     * <b><em>Note:</em></b>
     * Do <em>not</em> use this constructor for explicit creation of
     * instances of this class.
     * Instead, use the constructor that takes parameters.
     */
    private EbppxUrlManager() {
        // Default constructor
    }

    /**
     * Use this constructor to create instances of this Java Bean outside
     * of the serialization mechanism;
     *
     * @param   cipherAlgorithm the desired cipher algorithm.
     *          The recommended value is "3DES/PCBC/PKCS5",
     *          as defined by the constant <code>PREFERRED_CIPHER_ALGORITHM</code>.
     *          If a <code>null</code> value or an empty string ("") is passed,
     *          then no encryption will be used.
     *
     * @param   cipherKey   the secret encryption key.
     *          For the recommended cipher method,
     *          a 24-character key is required.
     *          For a <code>null</code> or empty <b>cipherAlgorithm</b>,
     *          this argument is ignored.
     *
     * @param   useSessionIntegrity indicates whether to include an extra
     *          session integrity request paramater on URL's to prevent
     *          cross-session browser bookmarking.
     */
    public EbppxUrlManager(
        HttpServletRequest request,
        HttpServletResponse response,
        String cipherAlgorithm,
        String cipherKey,
        boolean useSessionIntegrity
    ) {
        this.request = request;
        this.response = response;
        this.cipherAlgorithm = EbppxUtil.nullToEmpty(cipherAlgorithm);
        this.cipherKey = EbppxUtil.nullToEmpty(cipherKey);
        this.useSessionIntegrity = useSessionIntegrity;

        // Get the current session, if it exists.
        HttpSession session = request.getSession(false);
        if ((! useSessionIntegrity) || (session == null)) {
            sessionIntegrityValue = "";
        } else {
            long timestamp = session.getCreationTime();
            sessionIntegrityValue = String.valueOf(timestamp);
        }
    }

    private void ensureInitialization() throws ServletException {
        if ((request == null) || (response == null)) {
            throw new ServletException(
                    "Instances of the EbppxUrlManager bean must be explicitly constructed outside of a Java Server Page."
                    );
        }
    }

    private void ensureAlgorithm() throws ServletException {
        if ((cipherAlgorithm != null) && (cipherAlgorithm.equals(EbppxCipher.PREFERRED_CIPHER_ALGORITHM))) {
            if ((cipherKey == null) || (cipherKey.length() != EbppxCipher.PREFERRED_CIPHER_KEY_LENGTH)) {
                throw new ServletException(
                        "EbppxUrlManager bean has an unacceptable cipher key"
                        );
            }
        }
    }

    /**
     * Get the current name of the request parameter to be used
     * to hold the encrypted value of the entire actual query parameter string.
     *
     * @return  the current value of the request parameter name to be used.
     */
    public String getQueryEncryptionParameterName() {
        return queryEncryptionParameterName;
    }

    /**
     * Override the default name of the request parameter to be used
     * to hold the encrypted value of the entire actual query parameter string.
     * This property settor is optional;
     * a standard name will be used by default.
     *
     * @param   name    must be a non-null, non-empty text name that
     *                  can be used as a URL request parameter.
     */
    public void setQueryEncryptionParameterName(String name)
    throws ServletException {
        if (name == null) {
            throw new ServletException(
                    "Null query encryption parameter name not allowed for EbppxUrlManager"
                    );
        } else if (name.length() == 0) {
            throw new ServletException(
                    "Empty query encryption parameter name not allowed for EbppxUrlManager"
                    );
        } else {
            queryEncryptionParameterName = name;
        }
    }

    /**
     * Get the current name of the request parameter to be used to hold
     * the session integrity value.
     *
     * @return  the current value of the request parameter name to be used.
     */
    public String getSessionIntegrityParameterName() {
        return sessionIntegrityParameterName;
    }

    /**
     * Override the default name of the request parameter to be used to hold
     * the session integrity value.
     * This property settor is optional;
     * a standard name will be used by default.
     *
     * @param   name    must be a non-null, non-empty text name that
     *                  can be used as a URL request parameter.
     */
    public void setSessionIntegrityParameterName(String name)
    throws ServletException {
        if (name == null) {
            throw new ServletException(
                    "Null session integrity parameter name not allowed for EbppxUrlManager"
                    );
        } else if (name.length() == 0) {
            throw new ServletException(
                    "Empty session integrity parameter name not allowed for EbppxUrlManager"
                    );
        } else {
            sessionIntegrityParameterName = name;
        }
    }

    /**
     * Decrypt any encrypted request parameters from the incoming HTTP request.
     *
     * Any plain-text request parameters will be ignored (for now),
     * but will be also accessible by future calls to <code>getRequestParameter</code>.
     */
    public void decryptRequestParameters() throws ServletException {
        ensureInitialization();

        if ((cipherAlgorithm == null) || (cipherAlgorithm.length() == 0)) {
            decryptedParameters = null;
        } else {
            String encryptedQueryString =
                    request.getParameter(getQueryEncryptionParameterName());
            if ((encryptedQueryString == null) || (encryptedQueryString.length() == 0)) {
                decryptedParameters = null;
            } else {
                ensureAlgorithm();

                String decryptedQueryString = EbppxCipher.decryptToString(
                        cipherAlgorithm,
                        cipherKey,
                        encryptedQueryString,
                        1  // Remove header
                        );

                decryptedParameters = HttpUtils.parseQueryString(decryptedQueryString);

            } // else encrypted parameters are present

        } // else using encryption

        if (useSessionIntegrity) {
            // Protect against browser-bookmarking of an encrypted URL from
            // a prior session.
            String integrityCheckValue = getSecureParameter(
                    getSessionIntegrityParameterName()
                    );
            if ((integrityCheckValue.length() > 0) &&
                    (! integrityCheckValue.equals(sessionIntegrityValue))) {
                throw new ServletException(
                        "Encrypted URL query strings bookmarked from prior browser sessions are not allowed"
                        );
            }
        }
    }

    /**
     * Return a request parameter,
     * either from the plain-text parameters,
     * or the encrypted parameters.
     *
     * @param   name    the text name of the desired request parameter.
     *
     * @return  The desired parameter value,
     *          or an empty string ("") if the parameter is not present.
     *          Encrypted parameter values take precedence over
     *          plain-text parameter values.
     *          (This prevents URL <em>hacking</em> of appended values
     *          from bypassing the security of the encrypted values.)
     */
    public String getRequestParameter(String name) {
        return getRequestParameter(name, "");
    }

    /**
     * Return a request parameter,
     * either from the plain-text parameters,
     * or the encrypted parameters.
     *
     * @param   name The text name of the desired request parameter.
     *
     * @param   defaultValue The value to return if the parameter is not present.
     *
     * @return  The desired parameter value,
     *          or the passed <code>default</code> value
     *          if the parameter is not present.
     *          Encrypted parameter values take precedence over
     *          plain-text parameter values.
     *          (This prevents URL <em>hacking</em> of appended values
     *          from bypassing the security of the encrypted values.)
     */
    public String getRequestParameter(
        String name,
        String defaultValue
    ) {
        String value = null;

        if ((name != null) && (name.length() > 0)) {
            if (decryptedParameters != null) {
                Object valueObject = decryptedParameters.get(name);
                if (valueObject != null) {
                    String valueArray[] = (String[]) valueObject;
                    if (valueArray.length > 0) {
                        value = valueArray[0];
                    }
                }
            }

            if ((value == null) && (request != null)) {
                value = request.getParameter(name);
            }
        }

        if (value == null) {
            value = defaultValue;
        }

        return value;
    }

    /**
     * Return a request parameter from the encrypted parameters,
     * disallowing any value provided as a plain-text parameter.
     * <p>
     * <b><em>Special Case:</em></b>
     * If the cipher algorithm is "NONE",
     * so that no URL's are being encrypted,
     * the desired parameter will be accepted in plain-text.
     *
     * @param   name    The text name of the desired request parameter.
     *
     * @return  The desired parameter value
     *          (which <em>must</em> be an encrypted value),
     *          or an empty string ("") if the parameter is not present.
     *          (This prevents URL <em>hacking</em> in which a complete
     *          set of plain-text parameters replaces the encrypted values.)
     */
    public String getSecureParameter(String name) {
        return getSecureParameter(name, "");
    }

    /**
     * Return a request parameter from the encrypted parameters,
     * disallowing any value provided as a plain-text parameter.
     * <p>
     * <b><em>Special Case:</em></b>
     * If the cipher algorithm is "NONE",
     * so that no URL's are being encrypted,
     * the desired parameter will be accepted in plain-text.
     *
     * @param   name    The text name of the desired request parameter.
     *
     * @param   defaultValue The value to return if the parameter is not present.
     *
     * @return  The desired parameter value
     *          (which <em>must</em> be an encrypted value).
     *          (This prevents URL <em>hacking</em> in which a complete
     *          set of plain-text parameters replaces the encrypted values.)
     */
    public String getSecureParameter(
        String name,
        String defaultValue
    ) {
        String value = null;

        if ((name != null) && (name.length() > 0)) {
            if ((cipherAlgorithm == null) || (cipherAlgorithm.length() == 0)) {
                // Development mode -- not encrypting ANY parameters.
                value = request.getParameter(name);
            } else {
                // Production mode -- MUST encrypt this parameter
                if (decryptedParameters != null) {
                    Object valueObject = decryptedParameters.get(name);
                    if (valueObject != null) {
                        String valueArray[] = (String[]) valueObject;
                        if (valueArray.length > 0) {
                            value = valueArray[0];
                        }
                    }
                }
            }
        }

        if (value == null) {
            value = defaultValue;
        }

        return value;
    }

    /**
     * Process a URL string, encrypting all query parameters
     * (and encoding any cookie-less session identifier).
     *
     * @param   url The plain-text URL to be encrypted and encoded.
     *
     * @return  The processed URL, after applying encryption to the
     *          query parameters, and after applying
     *          <code>response.encodeURL</code> to the final result.
     */
    public String encryptURL(String url) throws ServletException {
        ensureInitialization();

        return response.encodeURL(encryptQueryStringInUrl(url));
    }

    /**
     * Process a URL string, encrypting all query parameters
     * (and redirection-encoding any cookie-less session identifier).
     *
     * @param   url The plain-text URL to be encrypted and encoded.
     *
     * @return  The processed URL, after applying encryption to the
     *          query parameters, and after applying
     *          <code>response.encodeRedirectURL</code> to the final result.
     */
    public String encryptRedirectURL(String url) throws ServletException {
        ensureInitialization();

        return response.encodeRedirectURL(encryptQueryStringInUrl(url));
    }

    private String encryptQueryStringInUrl(String url) throws ServletException {
        if ((url == null) || (url.length() == 0)) {
            return "";
        } else {
            int questionIndex = url.indexOf("?");
            if (questionIndex < 0) {
                return url;
            } else if ((cipherAlgorithm == null) || (cipherAlgorithm.length() == 0)) {
                String integrityUrl = "";
                if (useSessionIntegrity) {
                    integrityUrl = url + "&" +
                            getSessionIntegrityParameterName() + "=" +
                            sessionIntegrityValue;
                } else {
                    integrityUrl = url;
                }
                return integrityUrl;
            } else {  // We need to encrypt
                ensureAlgorithm();

                String queryParameters = "";
                if (useSessionIntegrity) {
                    queryParameters = getSessionIntegrityParameterName() + "=" +
                        sessionIntegrityValue + "&" +
                        url.substring(questionIndex + 1);
                } else {
                    queryParameters = url.substring(questionIndex + 1);
                }

                StringBuffer encryptedUrl =
                        new StringBuffer(url.substring(0, questionIndex));
                encryptedUrl.append("?");
                encryptedUrl.append(getQueryEncryptionParameterName());
                encryptedUrl.append("=");
                encryptedUrl.append(
                        URLEncoder.encode(
                            EbppxCipher.encryptFromString(
                                cipherAlgorithm,
                                cipherKey,
                                queryParameters,
                                1  // Include header
                                )
                            )
                        );
                return encryptedUrl.toString();
            }
        }
    }
}